home *** CD-ROM | disk | FTP | other *** search
- @CHAPTER TITLE = Working with PCX Files
-
- @FIRSTPAR = The Picture file format used by ZSoft for their PC Paintbrush
- paint program has become one of the more popular formats around. I
- personally took an interest in it originally because I needed to get
- diagrams into Ventura Publisher; which not only accepts .pcx files,
- but will also expand and contract them sensibly.
-
- The format turned out to be easy to work with. The files can be loaded
- and written quickly, using a set of simple functions. Screen update
- is fast, and the screen can easily be panned around a large picture.
- Drawing operations for multi-color formats aren't as fast, but that's
- due to the data structure that I've chosen.
-
- I'd like to start by describing PCX pictures in general terms. The
- Header and Color palette are complicated enough to deserve some more
- detail. I'll finish with a description of the modules that I've written
- to generate, write, read and display PCX pictures. You'll probably
- want to make many improvements to them, but I'm confidant that they'll
- get a PCX related project off the ground for you.
-
- @MAJOR HEADING = The Basic Structure
-
- A PCX file consists of a 128 byte header, followed by one or more
- compressed picture planes. In memory, each plane is basically a bit
- map, with each bit usually corresponding to a pixel on the screen. The
- number of bits per pixel, pixels per byte, bytes per line, lines per
- plane and planes per picture are all stored in the header.
-
- On the EGA board, there are four planes (see figure 1.) Bit (X,Y)
- in each plane corresponds to pixel (X,Y) on the screen. The binary
- value of the four (X,Y) bits in each plane is used to index into the
- color palette. On the CGA & HERC boards, only 1 plane is used. For
- the 720x348 HERC and 640x200 CGA modes, each bit in the plane corresponds
- to a single pixel on the screen. Using the 320x200 by 4 color CGA
- mode, there is again only one plane, but each pixel is made up of
- two adjacent bits.
-
- It's possible to make a 1000x1000 by 2 color drawing which can be
- displayed on a CGA board 640x200 dots at a time, then show it on a
- Hercules board 720x348 dots at a time, but a picture isn't always
- so portable. A 320x200 CGA picture has to be translated before it
- can be shown in another mode.
-
- As you can imagine, drawing the picture on the screen is easy <197>
- all that's needed is a block move of each line of the picture to the
- VRAM on the board. The trade off is that there has to be some interpretation
- of the header information, and the pictures don't look quite the same
- from one board to another due to changes in dot width and height.
-
- @MAJOR HEADING = The Header
-
- The structure for the header looks like this:
-
- @LISTING = typedef struct {<R>
- unsigned char red, green, blue;<R>
- } TRIPLET;<R>
- <R>
- typedef struct {<R>
- char maker,<R>
- version,<R>
- code,<R>
- bpp;<R>
- int x1, y1, x2, y2,<R>
- hres, vres;<R>
- TRIPLET triple[16];<R>
- char vmode, <R>
- nplanes;<R>
- int bpl;<R>
- char __unused[128-68];<R>
- } PCXHDR;
-
- The fields<B> maker<D>, <B>version<D>, <B>code<D> and <B>vmode<D>
- all currently have fixed values <197> 10, 5, 1, & 0. They represent
- the code for the program that generated the picture, its version number
- and the type of compression that was applied to the picture planes;
- <B>vmode<D> is currently ignored; and <B>__unused<D> is reserved for
- future expansion.
-
- <B>hres <D>and <B>vres<D> represent the number of dots, horizontal
- and vertical, on the video board to be used. <B>nplanes <D>is the
- number of picture planes used,<B> bpp<D> is the number of bits per
- pixel and <B>bpl<D> is the number of bytes that are used to store
- a horizontal line of the picture. (<B>x1,y1<D>)-(<B>x2,y2<D>) refer
- to the upper left and lower right corners of the picture. For a standard
- 640x350x16 EGA picture, we would get the following: <B>hres<D>=640,
- <B>vres<D>=350, <B>x1<D>=0, <B>y1<D>=0, <B>x2<D>=639, <B>y2<D>=349,
- <B>nplanes<D>=4, <B>bpp<D>=1 and <B>bpl<D>=80.
-
- It's interesting to note that PC PaintBrush seems to take these numbers
- very seriously. If a picture is made after PaintBrush is set up to
- use a HERC board, it won't even be recognized as a picture file if
- PaintBrush is then set up to use an EGA board. That's a bit picky
- considering that a HERC picture is black and white, and an EGA board
- supports those colors.
-
- So, if you plan to use the pictures that you generate on some existing
- program, be careful to model your picture after one of the various
- video board modes. To make a picture with a different size, only
- (<B>x2, y2<D>) and <B>bpl<D> would have to change; and the easiest
- way to add colors would be to add planes. Although the software that
- I've written only supports a few of the standard configurations, it'll
- probably be easy to modify.
-
- @MAJOR HEADING = The Color Palette
-
- The palette information is stored in the array of 16 triples. It's
- the only part of the PCX format that's really tricky <197> being totally
- board dependent. Perhaps Shannon at ZSoft can send Dave a letter explaining
- it better. Here's the way I understand it:
-
- The IBM EGA adapter has 16 single byte color registers, each of which
- holds a two bit intensity value of from 0 to 3 for each color gun
- (Red, Green, and Blue.) The intensity values are stored as pairs of
- bits, like this: "xxRGBrgb". The first two bits, 7 & 6, are ignored. The
- next three bits are the high bits of the intensity values for the
- Red, Green & Blue guns. The last three bits are the low bits of the
- same intensity values. This allows each of the 16 registers to contain
- one of 64 possible color combinations.
-
- The PCX header has a 16 element palette table, one element per color
- register. Each element is a three byte structure (TRIPLET) which
- holds an intensity value for each color gun. To set the color for
- any given palette register, the Red, Green & Blue intensity bytes
- are set with the desired intensity for that color multiplied by 85. (I
- have no idea why we multiply by 85.) To set the EGA palette register,
- we take the intensity value, divide by 85, twiddle the bits, then
- take the easy route to the palette register by calling the BIOS Set
- Palette function.
-
- The CGA board has a Color Select Register at I/O location 0x3d9. The
- bit configuration for this register is "xxCAIRGB". The first two
- bits are ignored. The C bit selects one of two pre-defined palettes,
- called Color Sets by IBM. Set 1 is Background, Green, Red & Brown. Set
- 2 is Background, Cyan, Magenta & White. The Background color is one
- of 16 colors formed using the IRGB bits. I is an intensity bit, R,
- G & B are enables for the Red, Green and Blue color guns, respectively. If
- the A bit is set, it "Selects (an) Alternate, Intensified set of colors
- in Grapics Mode" (see ref. 1.) To me, this translates into 4 color
- sets and one of 16 background colors.
-
- Storage of the CGA palette settings in the PCX header is simple: The
- first byte of the first element in the palette table contains a background
- color, multiplied by 16. The first byte of the second element in
- the palette table apparently contains a three bit value, multiplied
- by 32. (I haven't been able to figure out what to do with the third
- bit.) At any rate, there are 8 such settings in the PCX headers palette
- table, but it seems to me that only one of them can be in effect at
- a time.
-
- There are probably many more palette formats for other boards, but
- I don't know of any documentation for them. In the code that accompanies
- this article, the pcx_set_palette() function has a load_palette flag
- which must be set in order to get the routine to tinker with the palette
- registers. I didn't bother much with the palette for a number of
- reasons; the first one being that the palette is automatically set
- each time a graphics mode is selected; another being that Ventura
- Publisher ignores it.
-
- @MAJOR HEADING = Compressing a Plane
-
- When in a file, each plane in the picture is compressed. The compression
- is very simple; designed simply to take advantage of the fact that
- a byte value will often repeat.
-
- The read byte algorithm works like this: Read a byte. If the two
- high order bits are not set ((byte & 0xc0) != 0xc0), then return the
- byte. If the two high order bits are set ((byte & 0xc0) == 0xc0),
- then the remaining bits represent a repeat count (count = byte & 0x3f)
- and the next byte is the value that repeats. As such, the repeat
- count can never be greater than 0x3f, or 63. The write byte algorithm
- is obviously the opposite operation.
-
- @MAJOR HEADING = The Software
-
- Originally, what I wanted to do was copy schematics into Ventura. I
- sat in front of the system for many sleepless nights decoding DXF
- files, only to find in the end that the GEM files Ventura creates
- from the DXF files don't properly scale text.
-
- To make a long story short, I wrote most of a DXF to PCX converter,
- then (balking at the prospect of having to write a font editor, create
- fonts, and write the code to scale them,) scrapped the idea <197>
- it was less time consuming to just chop the schematics into small
- blocks. In the mean time, I'd written a (simple) board independent
- graphics package, several test routines, and a slide show program. As
- you've just guessed, text is not implemented.
-
- Everything was compiled using Manx Aztec 'C 86, V. 4.10a. With the
- exception of the code that handles flicker on the CGA board, it should
- compile without complaint on most systems. Note, however, that it'll
- only work in the large data model; not only because of the huge amount
- of RAM that it uses, but because of the way I accessed the VRAM.
-
- There isn't enough space to list all the code, so I'll document the
- software here, and you can download the files from one of the following
- sources: The file "$$$PCX.ARC" will be on the MicroC BBS long before
- you see this; and I'll put it in one of the IBM forums on Compu-Serve,
- in thanks to the many people who put together the EGA.ARC file (see
- ref. 2.) Finally, there'll be a copy in the LISTINGS/IBM.ARC section
- of BIX.
-
- @MAJOR HEADING = The Functions
-
- Basically, the functions can be divided into three groups. The lowest
- level is the board level driver modules. There's one for the HERC,
- EGA and CGA boards. To make it easy to use, the PCX module is set
- up as just another video board. Each module has the same collection
- of functions, each with the same name, although the function name
- is prefixed with the board name to keep them distinct.
-
- The next level is what I call the VGR modules, for Video GRaphics. It's
- a simple interface between the driver modules and the next higher
- level. This level is based on a bunch of macros to allow access to
- the driver level while still being able to switch drivers at run-time. Using
- these macros, object draw functions can be written which are independent
- of the low level drivers. So far, I've put in Bressenham's line drawing
- algorithm and an inefficient but functional irregular polygon fill.
- There's a circle drawing algorithm in the May '83 issue of Doctor
- Dobbs Journal which is likely to go in next.
-
- The highest level is the Picture level. The functions at this level
- operate on the following picture structure:
-
- @LISTING = typedef struct { <R>
- PCXHDR hdr;<R>
- char **rows[4];<R>
- } PCXPIC;
-
- (I feel I should warn you that the following few paragraphs are exceptionally
- boring. If you're not interested in how I chose to handle the pointer
- and bit twiddling details, skip to the next heading...)
-
- The PCXHDR is obvious enough, but the rows array deserves some explanation.
- Figure 2 shows how I visualize it when drawing a 640x350x16 EGA-type
- picture. A plane is a pointer to an array of rows, each of which
- is a pointer to an array of characters. That array of characters
- contains the bits which constitute a horizontal line of pixels.
-
- This approach is relatively inefficient in that it forces several
- 32 bit pointer operations, but it's easy to work with. As you can
- see, there's currently a hardwired limit of 4 planes, and the arrays
- are dynamically allocated.
-
- For all but the 320x200x4 CGA modes, we can read the bit at position
- (<B>x,y<D>) on a given plane using the following expression:
-
- @LISTING = !!(pcx_cpic<197>>>rows[plane][y][x>>>>3] & (0x80 >>>> (x & 7)));
-
- The global variable <B>pcx_cpic<D> is defined in the PCX module as
- a pointer to a PCXPIC picture (cpic == current picture) It's used
- to tell the pixel routines what picture to work on. For the HERC
- board, or the 640x200x2 CGA mode, plane is always zero; the 640x350x16
- EGA mode requires that the operation be repeated once for each plane. Since
- <B>rows[plane] <D>is a pointer to an array of rows, <B>rows[plane][y]
- <D>is a pointer to the Yth row. All that's needed now is the byte
- to look at in the row, and the bit in that byte. The low order three
- bits of <B>x<D> (<B>x<D> & 7) select the bit, and the remaining bits
- of <B>x<D> (<B>x<D> >>>> 3) become the byte offset for the row. The
- above expression would return 1 if the bit at (<B>x,y<D>) is set,
- or zero if it's not.
-
- The 320x200x4 CGA mode is slightly more complicated since two adjacent
- bits are used to select a color. The expression to return the color
- of the pixel at location (<B>x,y<D>) in that mode would be:
-
- @LISTING = i = (x & 0x03) <<<< 1;<R>
- color = (pcx<197>>>rows[0][y][x>>>>2] & (0xc0 >>>> i)) >>>> (6<197>i);
-
- Since each byte represents 4 pixels, <B>x<D>>>>>2 is used as the byte
- offset for the row, and <B>x<D> & 0x03 is the number of the pixel
- in the byte. Multiplying that number by 2 gives <B>i<D>, the number
- of bits from bit 7 that we skip over to get to the first of the two
- bits that select the color of the pixel. 0xc0 >>>> <B>i<D> makes a mask
- for those bits. The big difference between this expression and the
- previous one is that we need a two bit result, so we can't use the
- !! operator to clean up after the bit-wise AND. Shifting to the right
- 6<197><B>i<D> bits does the job.
-
- The only difference between the two expressions above and the ones
- actually used in the EGA and CGA modules is that there aren't any
- planes. The EGA board uses a plane register to enable each plane
- and all four planes use the same memory locations. The HERC board
- has two pages, which can be thought of as unrelated planes. I visualized
- each horizontal row of pixels as being an array of columns, so the
- arrays are called columns instead of rows. The array of pointers
- to each row is dynamically allocated. The pointers are initialized
- with sequential row addresses for the EGA board, and scattered addresses
- for the HERC and CGA boards.
-
- Although the pointer operations and bit twiddling never get more complicated
- than that, some of the expressions get a bit dense, and aren't much
- fun to read. Fortunately, all the low level operations are already
- implemented; you can just throw them into your library and use them.
- With that said, let's get down to function names and parameter lists...
-
- @MAJOR HEADING = Using the Functions
-
- Note that the declarations here don't match the ones in the modules.
- Many functions that don't return anything are declared as returning
- int. Most parameters to most of the functions are not range-checked.
-
- @PROTO = void allocf(char *p);<R>
- void map_not(int *map,int len);
-
- These two come out of my library. allocf() checks the pointer p to
- see if it's NULL. If so, it doesn't do anything. If not, it calls
- free(). If free() returns an error code, an error message is produced,
- and the program quits. map_not() inverts len ints in the bit-map
- pointed at by map.
-
- @PROTO = void cga_movmem(char far *s,char far *d,int n);<R>
- void cga_peekb(char far *p);<R>
- void cga_pokeb(char far *p,char b);
-
- These functions are the same as movmem(), peekb() and pokeb(); except
- that they wait for a horizontal retrace to start on the CGA board
- before accessing it, to avoid flicker. The cga_movmem() function is
- a straight block move, so you'll have to check for overlapping blocks
- if you plan to move a source to a destination which are both in the
- CGA VRAM.
-
- @PROTO = void ega_select_plane(int plane);<R>
- int pcx_select_plane(int plane);
-
- The EGA VRAM holds 4 planes, each of them mapped into the same memory
- space. ega_select_plane() is used to enable one or more planes. If
- it's passed a negative number, the absolute value of that number is
- used as an enable mask, ie: if <B>plane<D> == <197>0x0f, all 4 planes
- are simultaneously enabled. This is handy when clearing the screen
- since a single setmem() call can clear all four planes! If <B>plane<D>
- is not negative, then it must be from 0 to 3, and only that plane
- is enabled. pcx_select_plane() selects a plane for use by the VGR_ROW()
- macro when drawing on a PCX picture.
-
- @PROTO = void ega_set_palette(char reg,char red,char green,char blue);
-
- Sets one of the 16 EGA palette registers. <B>reg<D> can have a value
- of from 0 to 15. <B>red, green<D> and <B>blue<D> are the desired
- intensities for each color gun, for that <B>reg<D>ister. They must
- each have a value of from 0 to 3.
-
- @PROTO = int herc_set_page(int page);
-
- The HERC driver will default to page 0, but either of the two pages
- can be selected using this function.
-
- @PROTO = int *pcx_init(void);<R>
- cga_init(void);<R>
- ega_init(void);<R>
- herc_init(void);<R>
- <R>
- int VGR_HRES,<R>
- VGR_VRES,<R>
- VGR_NCOLORS,<R>
- VGR_NBPL; /* #bytes per line */
-
- The _init() routines initialize all static local variables in a module,
- allocate any required space (if it hasn't been allocated by a previous
- _init() call,) and set up the VGR module to use the module that was
- INITed. The VGR module, among other things, holds the four ints shown
- above. The HERC and EGA modules always set these values, as they're
- constants. The PCX module will set them only if pcx_cpic is non-null,
- therefore pointing to a valid PCXPIC picture. The CGA module sets
- them after a VGR_MODE() call. The _init() functions each return OK
- or ERROR, with ERROR usually meaning that an out of memory condition
- occurred.
-
- @PROTO = int VGR_MODE(int m);
-
- Once the VGR module has been set up, using the appropriate *_init()
- function, the "mode" must be set. Since the _init() call set up the
- array of pointers to functions in the VGR module, it's possible to
- use the #defined macros in vgr.h. VGR_MODE() is one of those macros.
- <B>m<D> must be one of the following:
-
- @LISTING = MODE_TEXT0 <197> text, 80x25<R>
- MODE_APA0 <197> APA, 640x350x16<R>
- MODE_APA1 <197> APA, 720x348x2<R>
- MODE_APA2 <197> APA, 640x200x2<R>
- MODE_APA3 <197> APA, 320x200x4
-
- When drawing on a PCX picture, the mode is only used to tell the PCX
- module if it's producing a one or two bit per pixel picture; so available
- PCX modes are effectively MODE_APA0 or MODE_APA3. Available EGA modes
- are MODE_TEXT0 or MODE_APA0; for the CGA it's MODE_TEXT0, MODE_APA2
- or MODE_APA3; and for HERC it's MODE_TEXT0 or MODE_APA1. ERROR or
- OK is returned, with ERROR indicating an invalid mode number.
-
- @PROTO = void VGR_CLEAR()<R>
- void VGR_SET(int x,int y,int c)<R>
- void VGR_CLR(int x,int y)<R>
- void VGR_XOR(int x,int y,int c)<R>
- int VGR_GET(int x,int y)<R>
- void VGR_ROW(int r,char far *p,int n)<R>
- void VGR_MOVE(char far *s,char far *d,int n)<R>
- void VGR_PEEKB(char far *p)<R>
- void VGR_POKEB(char far *p,int b)
-
- These VGR_ macros allow access to the board independent functions.
- _CLEAR clears the screen. _SET sets the color of the pixel at (<B>x,y<D>)
- to <B>c<D>. Note that if <B>c<D> has a value of zero, it has the effect
- of "clearing" the pixel. It's a good idea to not confuse the two operations
- since the HERC module ignores the value of <B>c<D>. _CLR sets the
- color of the pixel to zero. _XOR does a bit-wise exclusive or of the
- color of the pixel with <B>c<D>. Again, the HERC module ignores <B>c<D>.
- _GET returns the color of the pixel. _ROW moves <B>n<D> bytes from
- location <B>p<D> to the VRAM row <B>r<D>. _MOVE, _PEEKB & _POKEB either
- call movmem(), peekb() & pokeb(), or cga_movmem(), cga_peekb() & cga_pokeb();
- depending on the driver module in use.
-
- @PROTO = void vgr_fill(int x,int y,int color);<R>
- void vgr_line(int x1,int y1,int x2,int y2,int color);<R>
- void vgr_rectangle(int x1,int y1,int x2,int y2,int color);<R>
- void vgr_point(int x2,int y2,int color);
-
- vgr_line(), _rectangle() and _fill() do what they sound like. vgr_point()
- does one of two things. If <B>color<D> is -1, the values (<B>x2,y2<D>)
- are copied to the local static values (<B>x1,y1<D>). If <B>color<D>
- is not -1, vgr_point() calls vgr_line(), then copies (<B>x2,y2<D>)
- to (<B>x1,y1<D>). This makes it possible to draw lines between a
- series of points.
-
- @PROTO = int vgr_get_board(void);<R>
- int vgr_mode(char mode);
-
- vgr_get_board() attempts to figure out what kind of video graphics
- board is in use. The return value is one of TYPE_UNKNOWN, TYPE_EGA,
- TYPE_CGA, TYPE_MDA or TYPE_HERC. These values are defined in <B>vgr.h<D>.
- vgr_mode() calls the BIOS set graphic mode function; INT 0x10, AH==0.
- See ref. 2, page A-46 for more info on that BIOS call.
-
- @PROTO = PCXPIC *pcx_init_pic(int hres,int vres,int nplanes);<R>
- void pcx_free_pic(PCXPIC *pic);<R>
- void pcx_invert_pic(PCXPIC *pic);
-
- pcx_init_pic() dynamically allocates space for a PCXPIC structure,
- and returns a pointer to it. <B>hres<D> & <B>vres<D> are the number
- of dots horizontally and vertically; <B>nplanes<D> is the number of
- planes in the picture. All arrays are dynamically allocated, and a
- pointer to the result is returned. If an out of memory condition occurs,
- any allocated arrays are de-allocated, and NULL is returned. pcx_free_pic()
- is used to free up all the space allocated for a picture. pcx_invert_pic()
- inverts all the bits in each row of the picture.
-
- @PROTO = int pcx_read_pic(PCXPIC *pic,FILE *fp);<R>
- int pcx_getc(int *c,int *n,FILE *fp,int maxn);<R>
- int pcx_write_pic(PCXPIC *pic,FILE *fp);<R>
- int pcx_xputc(int c,FILE *fp);<R>
- int pcx_putc(int c,int n,FILE *fp);
-
- These functions operate on a file which has been opened by one of
- the STDIO buffered file open functions; <B>fp<D> is the file pointer
- returned by the open call. <B>pic<D> is a pointer to a PCXPIC picture
- structure. All these functions return OK or ERROR on failure.
-
- pcx_read_pic() loads a picture from the file. The pic structure can
- either be statically allocated, or dynamically allocated using the
- pcx_init_pic(0,0,0) call. pcx_read_pic() automatically allocates
- whatever arrays it needs to get the picture in. pcx_write_pic() writes
- the picture to the file.
-
- pcx_getc() reads a character or pair of characters from the file. The
- character is placed in the integer pointed at by <B>c<D>, and a repeat
- count of one or more is placed in the integer pointed at by <B>n<D>.
- <B>maxn<D> is the maximum repeat count that the calling function wants
- to receive.
-
- pcx_write_pic() calls pcx_xputc(), which counts the number of times
- it receives a given byte value, then calls pcx_putc() with that number.
- If pcx_xputc() is called with <B>c<D> == -1, the buffered byte value
- and count are passed immediately (flushed) to pcx_putc(). pcx_putc()
- writes the byte <B>c<D> to the file, using a repeat count of <B>n<D>.
- <B>n<D> may have a value of up to 32767; pcx_putc() will recursively
- call itself with <B>n<D><<=63 until the correct total has been written
- to the file.
-
- @PROTO = void pcx_showpic(PCXPIC *pic,int hoffs,int voffs,int load_palette_flg);
-
- This function uses VGR_ROW to copy rows from the <B>pic<D> picture
- to the video board driver module in use. <B>hoffs<D> is an offset
- from the start of each row in <B>pic<D>. <B>voffs<D> is an offset
- which is applied to the row number. These two offsets allow the screen
- to be panned around a large picture. The <B>load_palette_flg<D>, when
- set, allows pcx_showpic() to set the hardware palette registers from
- the PCXPIC palette table. Consider the palette functions to be unreliable.
-
- @MAJOR HEADING = The Test Routine
-
- As I'm writing this, PCX.EXE is bug free and running. It's essentially
- a test routine which excercises the various modules. If executed without
- command line parameters, it tries to figure out what kind of video
- board is available. If a CGA/HERC/EGA board is found, it creates a
- simple test picture (see figure 3,) displays it, then returns to DOS.
-
-
- If it's invoked with a board name, CGA (640x200x2), EGA, HERC or CGA2
- (320x200x4); it creates a picture for that board, and tries to display
- it on that adapter.
-
- If it's invoked with a board name followed by a file name, the picture
- is saved, using the specified file name, after it's been displayed.
-
- PCXSHOW.EXE, the slide show program, is in need of some upgrading
- <197> it was written before the latest improvements to the PCX/VGR
- modules. It'll be ready before you see this, and will be included
- with the other files.
-
- @MAJOR HEADING = Bibliography
-
- @BIBN = 1
-
- @BIB = The IBM Personal Computer XT Technical Reference Manual, April
- 1983 revision. Part Number 1502237.
-
- @BIBN = 2
-
- @BIB = The file EGA.ARC, which was found somewhere on Compu-Serve,
- probably in one of the IBM forums.
-
- @BIBN = 3
-
- @BIB = ZSoft Technical Reference Manual. ZSoft Corporation, 1950
- Spectrum Circle, Suite A-495, Marietta, GA 30067. Tel. (404) 428-0008.
- Many thanks to Shannon.
-
- @BIBN = 4
-
- @BIB = A Hercules Primer, by Larry Fogg, MicroCornucopia, Jan-Feb
- 1988, page 26.
-
- @BIBN = 5
-
- @BIB = Tidbits, by Gary Entsminger, MicroCornucopia, Jan-Feb '88,
- page 84.
-
- @BIBN = 6
-
- @BIB = Language Connections, by Gary Entsminger, Turbo Technix, Jan-Feb
- '88, page 136.
-
- @BIBN = 7
-
- @BIB = MicroEMACS by Dan Laurence & others. Public domain text editor,
- with 'C sources. Available on BIX in LISTINGS/IBM.ARC.
-